package com.tong.lettuce.lock.core;

import io.lettuce.core.RedisClient;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LettuceLock implements RedisLockable {

    private final Logger log = LoggerFactory.getLogger(LettuceLock.class);

    private static final String LOCK_PREFIX = "REDIS-LOCK:";

    private final String StatusOK = "OK";

    /**
     * 确保原子性, LUA脚本释放锁：如果value的值与参数相等,则删除,否则返回0
     */
    private static final String UNLOCK_SCRIPT_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    private RedisClient redisClient;

    public LettuceLock(RedisClient redisClient) {
        this.redisClient = redisClient;
    }

    /**
     * 添加锁, 按指定参数进行阻塞重试, 直到加锁成功
     * @param lockName 在 redis 中体现为 key: [LOCK_PREFIX + lockName]
     * @param value 存放到 key 之中, 用于【释放时验证】, 以防释放他人的锁
     * @param expireSeconds 超时秒数, 以防应用崩溃情况下的锁无法释放
     * @param retryCount 重试获取锁的次数
     * @param retryInterval 重试间隔
     * @return
     * @throws InterruptedException
     * @see {@link #tryLock(String, String, int)}
     */
    @Override
    public boolean lock(String lockName, String value, int expireSeconds, int retryCount, int retryInterval)
            throws InterruptedException {
        boolean result = false;
        for (int i = 0; i < retryCount; i++) {
            result = tryLock(lockName, value, expireSeconds);
            if (result) {
                log.debug("redis-lock [{}, {}] success", lockName, value);
                break;
            }
            log.debug("redis-lock [{}, {}] failed, wait retry...", lockName, value);
            Thread.sleep(retryInterval);
        }
        return result;
    }

    /**
     * 尝试添加锁, 无论成功失败, 立即返回结果
     * @param lockName 在 redis 中体现为 key: [LOCK_PREFIX + lockName]
     * @param value 存放到 key 之中, 用于【释放时验证】, 以防释放他人的锁
     * @param expireSeconds 超时秒数, 以防应用崩溃情况下的锁无法释放
     * @return
     */
    @Override
    public boolean tryLock(String lockName, String value, int expireSeconds) {
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisCommands<String, String> commands = connection.sync();
        String result = commands.set(LOCK_PREFIX + lockName, value, SetArgs.Builder.nx().ex(expireSeconds));
        log.debug("redis-lock try: [{}, {}, {}], result: {}", lockName, value, expireSeconds, result);
        return StatusOK.equals(result);
    }

    /**
     * 释放锁
     * @param lockName 在 redis 中体现为 key: [LOCK_PREFIX + lockName]
     * @param value 按此 value 进行验证, 以防释放他人的锁
     * @return
     */
    @Override
    public boolean unlock(String lockName, String value) {
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisCommands<String, String> commands = connection.sync();
        Long result = commands.eval(UNLOCK_SCRIPT_LUA, ScriptOutputType.INTEGER,
                new String[] { LOCK_PREFIX + lockName }, value);
        log.debug("redis-unlock: [{}, {}], result: {}", lockName, value, result);
        return result != null && result.intValue() == 1;
    }
}
